Popular Searches
Popular Course Categories
Popular Courses

Separating UI, Business Logic, and Data Layers

Separating UI, Business Logic, and Data Layers

Android Architecture and State Management

Separating UI, Business Logic, and Data Layers

Course: Android App Development

Topic: Separating UI, Business Logic, and Data Layers

Course Link: Android App Development Course

Demo Link: Register for a Course Demo

Website: JustAcademy


1. Introduction

Modern Android applications often combine user interfaces, API requests, database operations, authentication, validation, and business rules. If all these responsibilities are implemented inside a single Activity, Fragment, or Composable, the application becomes difficult to understand, test, maintain, and extend.

Separation of concerns is a software design principle that divides an application into components with clearly defined responsibilities. In Android development, this commonly means separating the UI layer, business logic, and data-access responsibilities.

The UI layer displays information and receives user interactions. Business logic determines how the application should behave according to its rules. The data layer manages access to information from sources such as web APIs, databases, files, and preferences.

Android's recommended architecture includes at least a UI layer and a data layer. An optional domain layer can be added when business logic is complex or shared across multiple ViewModels. Business logic may reside in the data layer or domain layer depending on its responsibility; the important goal is to keep it out of UI rendering code.

Learning Objectives

  • Understand separation of concerns in Android applications.
  • Identify the responsibilities of the UI, business logic, and data layers.
  • Understand the role of Activities, Fragments, Composables, and ViewModels.
  • Learn how repositories and data sources organize data access.
  • Understand the optional domain layer and use cases.
  • Apply unidirectional data flow and a single source of truth.
  • Implement layered architecture using Kotlin.
  • Manage asynchronous operations with Coroutines and Flow.
  • Understand dependency injection, testing, and common architectural mistakes.

2. What Is Separation of Concerns?

Separation of concerns means that each class, component, or layer should have a clear and limited responsibility. Instead of mixing screen rendering, validation rules, and database queries in one class, each responsibility is assigned to an appropriate component.

For example, in a shopping application, displaying a product price belongs to the UI, calculating a discount belongs to business logic, and retrieving product information from Room or a web API belongs to the data-access system.

Example of Poor Separation

class CheckoutActivity : AppCompatActivity() {

    fun checkout(price: Double, discount: Double) {
        val finalPrice = price - discount

        // Perform a network request here.
        // Insert an order into the database here.
        // Update multiple views here.
        // Display success or error messages here.
    }
}

This example places price calculation, network communication, database access, and UI behavior in one Activity. As the application grows, this class becomes harder to test and maintain.

Example of Better Separation

// UI layer:
// Displays the checkout state and forwards user actions.

// Business logic:
// Calculates the final price and validates checkout rules.

// Data layer:
// Retrieves product information and saves orders.

// ViewModel:
// Coordinates the screen state and exposes it to the UI.

Each component now has a more focused responsibility. This structure makes individual components easier to modify and test without changing unrelated parts of the application.

3. Overview of the Main Application Layers

Layer Primary Responsibility Typical Components
UI Layer Displays information and accepts user interactions. Activity, Fragment, Compose UI, ViewModel, UI state.
Business Logic Applies application rules and coordinates operations. Validation functions, pricing rules, use cases, domain services.
Data Layer Provides and modifies application data. Repositories, API services, Room DAOs, data sources.

These are logical responsibilities, not necessarily three separate Gradle modules. A small Android application can implement them using packages in one module. Larger applications may separate them into modules to strengthen boundaries and improve build organization.

4. Understanding the UI Layer

The UI layer, also called the presentation layer, is responsible for displaying application information and receiving user interactions. It converts application state into visible elements such as text, buttons, lists, progress indicators, and error messages.

Responsibilities of the UI Layer

  • Display information supplied by the application state.
  • Receive clicks, text input, selections, and other user actions.
  • Forward actions to the ViewModel or another state holder.
  • Observe state updates and refresh the screen.
  • Show loading, success, empty, and error states.
  • Manage visual behavior and navigation presentation.

Components of the UI Layer

Activity: An Android component that hosts an application screen or navigation container.

Fragment: A reusable UI controller that can host part of an Activity's interface.

Composable: A Kotlin function used to describe UI elements when using Jetpack Compose.

ViewModel: A state holder that manages screen-related data and coordinates actions without directly manipulating individual UI widgets.

UI State: A representation of the information needed to render the screen, such as a product list, loading status, or error message.

Example: Simple UI Responsibility

data class ProductUiModel(
    val name: String,
    val formattedPrice: String
)

fun displayProduct(product: ProductUiModel) {
    // Render product.name and product.formattedPrice
    // using the application's UI framework.
}

The UI receives data that is already suitable for presentation. It should not need to know how the product was retrieved from a server or how the price was stored in the database.

5. Understanding Business Logic

Business logic contains the rules that determine how an application behaves. These rules represent the purpose of the application and should generally remain independent of specific screen layouts.

For an e-commerce application, business logic might include calculating discounts, checking whether an order qualifies for free delivery, validating stock availability, or determining whether a payment operation is allowed.

Examples of Business Logic

  • Calculate a discount based on a customer's membership level.
  • Prevent an order quantity from exceeding available stock.
  • Validate whether a username meets application requirements.
  • Calculate delivery charges based on order value.
  • Determine whether a user is eligible for a reward.
  • Apply rules before confirming a payment or booking.

Pure Business Logic Example

data class PriceBreakdown(
    val originalPrice: Double,
    val discount: Double,
    val finalPrice: Double
)

fun calculatePrice(
    originalPrice: Double,
    discountPercent: Double
): PriceBreakdown {
    require(originalPrice >= 0.0) {
        "Price cannot be negative"
    }

    require(discountPercent in 0.0..100.0) {
        "Discount must be between 0 and 100"
    }

    val discount = originalPrice * discountPercent / 100.0

    return PriceBreakdown(
        originalPrice = originalPrice,
        discount = discount,
        finalPrice = originalPrice - discount
    )
}

This function applies pricing rules without depending on an Activity, Fragment, Android Context, or database. It can be tested using ordinary Kotlin unit tests.

Production note: For financial calculations, prefer an appropriate money representation such as integer minor units or a decimal type with a defined rounding policy rather than relying on floating-point arithmetic.

6. Understanding the Data Layer

The data layer is responsible for providing application data and handling operations that create, retrieve, update, or delete it. It commonly contains repositories and data sources.

Responsibilities of the Data Layer

  • Retrieve data from remote APIs.
  • Read and write local database records.
  • Store application preferences or settings.
  • Coordinate data from multiple sources.
  • Provide a consistent interface to the rest of the application.
  • Handle caching, synchronization, and source-selection rules when required.
  • Convert external data representations into models suitable for the application.

Common Data Sources

Data Source Technology Examples Purpose
Remote service Retrofit, HTTP APIs Retrieve or update information on a server.
Local database Room, SQLite Store structured data locally.
Preferences DataStore Store application settings and small preference values.
File storage Android file APIs Read and write supported files.
Cloud data service Firebase services Access supported cloud data and application services.

The UI and ViewModel should normally request data through repository interfaces rather than directly querying a database or calling an API client. This keeps data-access details isolated from screen-specific behavior.

7. What Is a Repository?

A repository provides a consistent interface for accessing and modifying a particular category of application data. It hides the details of the underlying data sources from the UI and business logic that consume it.

For example, a ProductRepository might retrieve products from a remote API, read cached products from Room, or coordinate both sources. A ViewModel can request products without knowing which source supplied them.

Repository Responsibilities

  • Expose application data through suitable functions or Flows.
  • Centralize data-access operations.
  • Coordinate local and remote sources.
  • Implement appropriate caching or synchronization strategies.
  • Hide implementation details of API clients and database access.
  • Provide a convenient boundary for unit testing.

Repository Interface Example

interface ProductRepository {
    suspend fun getProducts(): List<Product>
}

The interface defines what consumers can request without specifying how the data is retrieved.

8. What Is a Data Source?

A data source communicates with one particular source of data. A remote data source may call a web API, while a local data source may use Room to access stored records.

Repositories can depend on one or more data sources. The repository decides how their results are exposed to the rest of the application.

interface ProductRemoteDataSource {
    suspend fun fetchProducts(): List<Product>
}

interface ProductLocalDataSource {
    suspend fun readProducts(): List<Product>
    suspend fun saveProducts(products: List<Product>)
}

These interfaces define the operations available to a repository. Concrete implementations would use a network library, Room DAO, or another appropriate storage mechanism.

9. Understanding the Optional Domain Layer

The domain layer is an optional layer between the UI and data layers. It is useful when application rules are complex, when operations must be reused across several ViewModels, or when a ViewModel would otherwise coordinate too many business operations.

Domain-layer classes are commonly called use cases or interactors. Each use case should have one clearly defined responsibility, such as calculating an order total, retrieving a user's profile, or checking whether a purchase is eligible for a discount.

When to Add a Domain Layer

  • Several screens reuse the same business operation.
  • Business rules are complex and need independent testing.
  • A ViewModel has become difficult to understand because it coordinates many operations.
  • Application logic must be reused by different presentation components.

A domain layer is not mandatory for every project. In a small application, simple rules may remain in the data layer or ViewModel as appropriate. Add layers to solve real complexity rather than creating unnecessary classes.

Use Case Example

class CalculateDiscountUseCase {

    operator fun invoke(
        price: Double,
        discountPercent: Double
    ): Double {
        require(price >= 0.0)
        require(discountPercent in 0.0..100.0)

        return price * discountPercent / 100.0
    }
}

The use case encapsulates one business operation. A ViewModel can call it without needing to know the details of its implementation.

10. How the Layers Communicate

In a typical Android application, the UI sends a user action to the ViewModel. The ViewModel requests the necessary operation from a repository or use case. The data layer retrieves or modifies the data and returns a result. The ViewModel converts that result into UI state, and the UI renders the updated state.

Layered Architecture Flow

                 USER ACTION
                      |
                      v
              +----------------+
              |   UI Layer     |
              | Activity /     |
              | Fragment /     |
              | Compose UI     |
              +----------------+
                      |
              Action or function call
                      |
                      v
              +----------------+
              |   ViewModel    |
              | Screen state   |
              | Coordination   |
              +----------------+
                      |
                Optional use case
                      |
                      v
              +----------------+
              | Domain Layer   |
              | Business rules |
              +----------------+
                      |
                      v
              +----------------+
              |  Data Layer    |
              | Repository     |
              +----------------+
                      |
             +--------+--------+
             |                 |
             v                 v
       Remote API         Local Database
             |                 |
             +--------+--------+
                      |
                      v
               Updated result
                      |
                      v
               ViewModel state
                      |
                      v
                 Updated UI

The domain layer is optional. When it is omitted, the ViewModel can interact with the repository directly. The UI should not depend on the implementation details of the data sources.

11. Understanding Unidirectional Data Flow

Unidirectional Data Flow (UDF) is a design approach in which state flows toward the UI, while user actions flow back to the component responsible for updating that state.

Instead of allowing multiple UI components to modify shared screen data independently, the ViewModel or another state holder processes actions and exposes an updated state. The UI observes that state and renders it.

UDF Process

  1. The UI displays the current state.
  2. The user performs an action, such as selecting a product.
  3. The UI sends the action to the ViewModel.
  4. The ViewModel processes the action and requests data or applies business rules.
  5. The ViewModel publishes the new UI state.
  6. The UI observes the new state and redraws the relevant content.

Example

class CounterViewModel : ViewModel() {

    private val _count = MutableStateFlow(0)
    val count: StateFlow<Int> = _count.asStateFlow()

    fun onIncrementClicked() {
        _count.value += 1
    }
}

The UI calls onIncrementClicked() when the user presses the button. The ViewModel updates the state, and the UI renders the new count by observing count.

12. Understanding the Single Source of Truth

The Single Source of Truth (SSOT) principle means that each piece of application data has a clearly defined authoritative owner. Instead of maintaining unrelated copies of the same information in several places, other components observe or request the data from its designated source.

For example, if Room is the authoritative local source for saved products, the application can observe the database and update the screen when records change. A repository can coordinate server synchronization while keeping data ownership and update rules clear.

Benefits of SSOT

  • Reduces inconsistent copies of data.
  • Makes updates easier to track.
  • Improves predictability of UI state.
  • Simplifies debugging and testing.
  • Supports offline-first designs when paired with appropriate persistence.

Single source of truth does not mean an application can use only one storage technology. It means each type of data should have a clearly defined authoritative owner and synchronization policy.

13. Project Structure for Layered Android Architecture

A clear package structure helps developers find code and understand each component's responsibility. The following example shows one possible organization for a product application.

com.example.shop
|
+-- ui
|   +-- products
|   |   +-- ProductScreen.kt
|   |   +-- ProductViewModel.kt
|   |   +-- ProductUiState.kt
|
+-- domain
|   +-- model
|   |   +-- Product.kt
|   +-- usecase
|       +-- GetProductsUseCase.kt
|
+-- data
|   +-- repository
|   |   +-- ProductRepositoryImpl.kt
|   +-- remote
|   |   +-- ProductApi.kt
|   |   +-- ProductRemoteDataSource.kt
|   +-- local
|       +-- ProductDao.kt
|       +-- ProductLocalDataSource.kt
|
+-- di
    +-- AppModule.kt

This is an example, not a mandatory structure. Smaller applications can use fewer packages, and larger applications can organize code into separate Gradle modules. Keep the boundaries understandable and avoid creating unnecessary layers for trivial operations.

14. Practical Example: Product Data Model

Begin by defining a model that represents product information used by the application.

data class Product(
    val id: Int,
    val name: String,
    val price: Double
)

This model is intentionally simple. A production application may use separate network DTOs, database entities, domain models, and UI models when their requirements differ. For a small application, reusing a model may be acceptable when the boundaries remain clear.

15. Implementing the Data Layer

Define a repository interface so that the ViewModel does not need to depend directly on the implementation of an API or database.

Repository Contract

interface ProductRepository {
    suspend fun getProducts(): List<Product>
}

Repository Implementation

class ProductRepositoryImpl : ProductRepository {

    override suspend fun getProducts(): List<Product> {
        // Replace this sample with a real data source.
        return listOf(
            Product(1, "Android Phone", 25000.0),
            Product(2, "Laptop", 65000.0),
            Product(3, "Tablet", 30000.0)
        )
    }
}

This sample repository returns in-memory data. In a real application, the implementation could use Retrofit, Room, or both. Keeping the repository behind an interface makes it easier to replace the implementation in tests.

16. Implementing Business Logic with a Use Case

Suppose the application needs to display products below a specified price. This rule can be implemented as a use case when it is meaningful to the domain or reused by multiple screens.

class GetAffordableProductsUseCase(
    private val repository: ProductRepository
) {
    suspend operator fun invoke(
        maximumPrice: Double
    ): List<Product> {
        require(maximumPrice >= 0.0) {
            "Maximum price cannot be negative"
        }

        return repository.getProducts().filter { product ->
            product.price <= maximumPrice
        }
    }
}

The use case applies the filtering rule without knowing how the products are displayed. It depends on the repository contract rather than a specific network or database implementation.

17. Implementing the ViewModel

The ViewModel coordinates the screen operation and exposes an observable state. The UI does not need to know how the repository obtains product information.

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

sealed interface ProductUiState {
    data object Loading : ProductUiState
    data class Success(
        val products: List<Product>
    ) : ProductUiState
    data class Error(
        val message: String
    ) : ProductUiState
}

class ProductViewModel(
    private val getAffordableProducts: GetAffordableProductsUseCase
) : ViewModel() {

    private val _uiState =
        MutableStateFlow<ProductUiState>(ProductUiState.Loading)

    val uiState: StateFlow<ProductUiState> =
        _uiState.asStateFlow()

    fun loadProducts(maximumPrice: Double) {
        viewModelScope.launch {
            _uiState.value = ProductUiState.Loading

            try {
                val products = getAffordableProducts(maximumPrice)
                _uiState.value = ProductUiState.Success(products)
            } catch (exception: CancellationException) {
                throw exception
            } catch (exception: Exception) {
                _uiState.value = ProductUiState.Error(
                    exception.message ?: "Unable to load products"
                )
            }
        }
    }
}

This example assumes the use case and repository are provided through a compatible ViewModel factory or dependency-injection framework. Cancellation exceptions are rethrown so coroutine cancellation behaves correctly.

18. Rendering the UI from State

The UI collects the ViewModel state and decides what to display. With Jetpack Compose, a lifecycle-aware collection helper can observe the StateFlow and trigger recomposition when the state changes.

@Composable
fun ProductScreen(
    viewModel: ProductViewModel
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()

    when (val state = uiState) {
        is ProductUiState.Loading -> {
            CircularProgressIndicator()
        }

        is ProductUiState.Success -> {
            LazyColumn {
                items(state.products) { product ->
                    Text(
                        text = "${product.name} - ${product.price}"
                    )
                }
            }
        }

        is ProductUiState.Error -> {
            Text(text = state.message)
        }
    }
}

This snippet assumes the required Compose, lifecycle-compose, and Material imports are present. The ViewModel must also be created with the dependencies required by its constructor. In a real project, format prices appropriately for the user's locale and currency.

19. Handling Loading, Success, Empty, and Error States

A good UI state model accounts for the major outcomes of a data operation. It should not assume that every request succeeds or always returns data.

State Meaning Possible UI Behavior
Loading Data is being retrieved or processed. Show a progress indicator.
Success Data was retrieved successfully. Display the results.
Empty The operation succeeded but there are no results. Show an empty-state message or suggestion.
Error The operation could not complete successfully. Show a useful message and possibly a retry action.

An Empty state can be represented as a separate sealed state or derived from a successful result containing an empty list. Choose the representation that makes the screen logic clear.

20. Using Coroutines and Flow Between Layers

Kotlin Coroutines and Flow help layers communicate when operations take time or data changes over time. A suspending repository function is appropriate for a one-time operation, while Flow is useful for observing updates.

One-Time Data Request

interface UserRepository {
    suspend fun getUser(): User
}

A suspending function returns one result per invocation, or throws an exception if the operation fails.

Observable Data Stream

interface UserRepository {
    fun observeUser(): Flow<User>
}

A Flow can emit multiple values over time. For example, a Room DAO may return a Flow that emits new query results when relevant database records change.

Choosing Between suspend and Flow

Requirement Suitable Approach
Fetch a profile once. suspend fun getProfile()
Observe database changes. Flow<T>
Expose the latest screen state. StateFlow<T>
Broadcast shared events. SharedFlow<T>, when appropriate

21. Dependency Injection and Layer Separation

Dependency injection provides a class with the objects it needs instead of requiring it to create every dependency internally. This helps maintain clear boundaries between the ViewModel, use cases, repositories, and data sources.

Dependencies can be provided manually for small applications or through frameworks such as Hilt for larger applications.

Manual Dependency Construction

val repository: ProductRepository = ProductRepositoryImpl()

val useCase = GetAffordableProductsUseCase(repository)

// A ViewModel factory can provide useCase to ProductViewModel.

The example demonstrates the dependency relationship; an actual Activity or Compose screen should obtain the ViewModel through an appropriate lifecycle-aware creation mechanism.

Benefits of Dependency Injection

  • Reduces hard-coded dependencies.
  • Allows fake repositories to be used in tests.
  • Centralizes dependency creation.
  • Supports consistent object lifetimes where needed.
  • Makes implementation changes easier.

22. Testing Each Layer Independently

Layered architecture improves testability because each component has a smaller responsibility. Business rules can be tested without Android UI classes, repositories can be tested with fake data sources, and ViewModels can be tested by providing fake use cases or repositories.

Business Logic Test

import org.junit.Assert.assertEquals
import org.junit.Test

class PriceCalculationTest {

    @Test
    fun calculatesDiscountCorrectly() {
        val result = calculatePrice(
            originalPrice = 1000.0,
            discountPercent = 10.0
        )

        assertEquals(900.0, result.finalPrice, 0.001)
    }
}

Repository Test Ideas

  • Verify that the repository returns expected product data.
  • Verify that API failures are handled according to the repository contract.
  • Verify that local data is saved when required.
  • Verify the behavior when local and remote sources disagree.

ViewModel Test Ideas

  • Verify the initial UI state.
  • Verify that successful data retrieval produces Success state.
  • Verify that failures produce an appropriate Error state.
  • Verify that user actions trigger the expected use case.

23. Common Architectural Mistakes

Mistake Why It Is a Problem Recommended Solution
Putting everything in an Activity Creates a large class with mixed responsibilities. Move screen state to ViewModel and data access to repositories.
Calling the API directly from a Composable Couples UI rendering to data retrieval and can trigger repeated work. Call ViewModel actions and expose observable state.
Accessing Room directly from ViewModel Couples screen logic to a specific persistence implementation. Use a repository to expose the data operation.
Creating unnecessary domain classes Adds boilerplate without solving a real problem. Add use cases when logic is complex or reused.
Duplicating the same business rules Different screens can produce inconsistent results. Centralize reusable rules in a suitable domain or data-layer component.
Making every class depend on concrete implementations Makes replacement and isolated testing harder. Use interfaces where they provide meaningful flexibility.
Exposing mutable internal state Consumers can modify data without following the intended rules. Expose immutable models and read-only state streams.
Ignoring errors Users may see stale content or an endless loading indicator. Model and render error states explicitly.
Using a global singleton for every dependency Can introduce hidden coupling and difficult lifecycle behavior. Define appropriate dependency lifetimes and ownership.
Creating too many layers in a small app Increases complexity without providing meaningful benefits. Use the simplest architecture that maintains clear boundaries.

24. Best Practices for Layered Android Architecture

  • Keep the UI focused on rendering state and forwarding user actions.
  • Use ViewModel as a screen state holder where appropriate.
  • Expose application data from the data layer through repositories.
  • Keep data-source implementation details out of the UI.
  • Use a domain layer when business operations are complex or reused.
  • Prefer immutable UI state and controlled state updates.
  • Use Coroutines and Flow for asynchronous and observable data operations.
  • Follow unidirectional data flow to make state changes predictable.
  • Define a clear source of truth for each important piece of data.
  • Use dependency injection or factories to manage dependencies.
  • Write unit tests for business rules, repositories, and ViewModels.
  • Handle errors, empty results, and cancellation explicitly.
  • Keep architecture proportional to the size and complexity of the application.

25. Practical Project: Product Catalog Application

Project Goal: Create a product catalog that displays products, filters them by price, and handles loading and error states using separate application layers.

Step 1: Create the Product Model

data class Product(
    val id: Int,
    val name: String,
    val price: Double
)

Step 2: Define the Repository

interface ProductRepository {
    suspend fun getProducts(): List<Product>
}

Step 3: Implement the Repository

class FakeProductRepository : ProductRepository {

    override suspend fun getProducts(): List<Product> {
        return listOf(
            Product(1, "Phone", 25000.0),
            Product(2, "Laptop", 65000.0),
            Product(3, "Headphones", 2500.0)
        )
    }
}

Step 4: Create a Use Case

class GetProductsUnderPriceUseCase(
    private val repository: ProductRepository
) {
    suspend operator fun invoke(
        maxPrice: Double
    ): List<Product> {
        require(maxPrice >= 0.0)

        return repository.getProducts().filter {
            it.price <= maxPrice
        }
    }
}

Step 5: Create the ViewModel

class ProductCatalogViewModel(
    private val getProductsUnderPrice: GetProductsUnderPriceUseCase
) : ViewModel() {

    private val _uiState =
        MutableStateFlow<ProductUiState>(ProductUiState.Loading)

    val uiState: StateFlow<ProductUiState> =
        _uiState.asStateFlow()

    fun load(maxPrice: Double) {
        viewModelScope.launch {
            _uiState.value = ProductUiState.Loading

            try {
                val products = getProductsUnderPrice(maxPrice)
                _uiState.value = ProductUiState.Success(products)
            } catch (exception: CancellationException) {
                throw exception
            } catch (exception: Exception) {
                _uiState.value = ProductUiState.Error(
                    exception.message ?: "Unable to load catalog"
                )
            }
        }
    }
}

This example reuses the Product model and ProductUiState type defined earlier. It also assumes the appropriate imports and a ViewModel factory or dependency-injection setup.

Step 6: Connect the UI

Collect uiState using lifecycle-aware APIs. Display a progress indicator during Loading, show the products during Success, and provide a useful retry option during Error.

Expected Outcomes

  • Product data is retrieved through the repository.
  • Filtering rules are implemented separately from the UI.
  • The ViewModel coordinates the operation and publishes screen state.
  • The UI displays state rather than directly accessing the data source.
  • Each layer can be tested or modified independently.

26. Exercises for Practice

  1. Create an Android screen that displays a list of products from a repository.
  2. Move product filtering rules from the Activity into a use case.
  3. Create a repository interface and a fake implementation for unit tests.
  4. Implement Loading, Success, Empty, and Error UI states.
  5. Expose ViewModel state through StateFlow and collect it using lifecycle-aware APIs.
  6. Replace the fake repository with a Room-backed implementation.
  7. Add a remote API data source and decide how local and remote data should be coordinated.
  8. Write unit tests for discount calculations and product filtering.
  9. Use dependency injection or a ViewModel factory to provide required dependencies.
  10. Review an existing application and identify code that violates separation of concerns.

27. Interview Questions and Answers

Q1. What is separation of concerns?

It is a design principle that assigns different responsibilities to separate components so that each component can be understood, modified, and tested more independently.

Q2. What is the responsibility of the UI layer?

The UI layer displays application state, receives user interactions, and forwards actions to the appropriate state holder or application component.

Q3. What is business logic?

Business logic represents the rules that determine how an application behaves, such as price calculations, eligibility checks, and order validation.

Q4. What is the data layer?

The data layer provides application data and handles operations involving sources such as remote APIs, local databases, and preferences.

Q5. What is a repository?

A repository provides a consistent interface to application data and hides the details of underlying data sources from consumers.

Q6. What is the domain layer?

The domain layer is an optional layer that encapsulates complex or reusable business operations, often through use cases.

Q7. What is unidirectional data flow?

It is an approach in which state flows toward the UI and user actions flow back to the component responsible for processing those actions and updating state.

Q8. What is a single source of truth?

It is a principle in which each piece of application data has a clearly defined authoritative owner, helping prevent conflicting copies of the same information.

Q9. Why should a ViewModel avoid calling an API client directly?

Using a repository separates data access from screen logic, improves testability, and prevents the ViewModel from depending on a particular API implementation.

Q10. Is a domain layer mandatory in every Android application?

No. Android architecture guidance treats the domain layer as optional. It is useful when business logic is complex or shared across multiple ViewModels.

Q11. What is dependency injection?

Dependency injection supplies a class with the objects it needs rather than making it create all its dependencies internally.

Q12. How do Coroutines and Flow help layered architecture?

Coroutines support asynchronous operations, while Flow supports streams of values over time. They help layers exchange data without requiring synchronous blocking operations.

28. Quick Revision Checklist

  • Separation of concerns gives each component a clear responsibility.
  • The UI layer renders application state and forwards user actions.
  • Business logic applies the rules of the application.
  • The data layer handles access to application data.
  • Repositories provide a consistent interface to data sources.
  • The domain layer is optional and useful for complex or reused logic.
  • ViewModels manage screen state and coordinate operations.
  • Unidirectional data flow makes state changes more predictable.
  • A single source of truth helps avoid conflicting copies of data.
  • Coroutines and Flow support asynchronous data communication.
  • Dependency injection makes components easier to replace and test.
  • Layered architecture should match the actual complexity of the project.

29. Conclusion

Separating UI, business logic, and data responsibilities is a fundamental practice for building maintainable Android applications. A well-organized architecture allows the UI to focus on presentation, the ViewModel to manage screen state, business components to apply application rules, and repositories to coordinate data sources.

When combined with unidirectional data flow, Coroutines, Flow, dependency injection, and unit testing, this approach makes applications easier to debug, extend, and test. The goal is not to create as many layers as possible, but to establish clear boundaries that make the application reliable and understandable as it grows.

30. Learning Resources

Previous Next
whatsapp